<?php
namespace WDB\Query;
use WDB,
    WDB\Exception;

/**
 *
 * @author Richard Ejem <richard(at)ejem.cz>
 * @package WDB
 *
 * @property Element\SelectField[] $fields
 * @property Element\iExpression[] $group
 * @property Element\iCondition $having
 */
final class Select extends SUDQuery implements WDB\iDatasource, Element\iExpression, Element\iTableSource
{
    /**@var Element\SelectField[]*/
    private $fields;
    /**@var \WDB\Query\Select*/
    private $union = NULL;
    /**@var bool*/
    private $unionAll = TRUE;
    /**@var Element\iExpression[]*/
    private $group = array();
    /**@var Element\iCondition|NULL*/
    private $having = NULL;

    //datasource configuration variables
    /**@var int*/
    private $page;
    /**@var int*/
    private $pageSize;
    /**@var bool */
    private $distinct = FALSE;


    /**
     * Create the select query class.
     *
     * @param Element\SelectField[]|bool fields to select or TRUE if all columns should be selected
     * @param Element\iTableSource source table
     * @param Element\iCondition selection condition
     * @param Element\OrderRule[] ordering rules
     * @param Element\iExpression[] GROUP BY clause expressions
     * @param Element\iCondition $having HAVING condition
     * @param int maximum number of retrieved records
     * @param int start retrieving at offset
     */
    public function __construct($fields, $table, $where = NULL, $order = array(), $group = array(), $having = NULL, $limit = 0, $offset = 0)
    {
        parent::__construct();
        $this->fields = array();
        $this->setFields($fields);


        $this->setTable($table);

        $this->setWhere($where);

        if (is_array($order))
        {
            foreach ($order as $o)
            {
                $this->appendOrder($o);
            }
        }
        else
        {
            $this->appendOrder($o);
        }

        if (is_array($group))
        {
            foreach ($group as $g)
            {
                $this->addGroup($g);
            }
        }
        else
        {
            $this->addGroup($g);
        }
        $this->setHaving($having);
        $this->setLimit($limit);
        $this->setOffset($limit);

    }

    public function join($table, $condition = Element\Join::NATURAL, $type = Element\Join::INNER)
    {
        $this->table = new Element\Join($this->table, $table, $condition, $type);
        return $this;
    }

    public function ljoin($table, $condition = Element\Join::NATURAL)
    {
        return $this->join($table, $condition, Element\Join::LEFT);
    }

    public function rjoin($table, $condition = Element\Join::NATURAL)
    {
        return $this->join($table, $condition, Element\Join::RIGHT);
    }

    public function ojoin($table, $condition = Element\Join::NATURAL)
    {
        return $this->join($table, $condition, Element\Join::OUTER);
    }

    //<editor-fold defaultstate="collapsed" desc="select field list manipulation">
    /**
     * Add field to select field list.
     *
     * @param Element\iExpression|Element\SelectField
     * @return Query
     */
    public function addField($field)
    {
        if (is_string($field))
        {
            $field = Element\ColumnIdentifier::parse($field);
        }
        if ($field === Element\ColumnIdentifier::ALL_COLUMNS)
        {
            $field = Element\ColumnIdentifier::allColumns();
        }
        if ($field instanceof Element\iExpression)
        {
            $field = new Element\SelectField($field);
        }

        if (!$field instanceof Element\SelectField)
        {
            throw new Exception\BadArgument(__METHOD__.'accepts only Element\SelectField or Element\iExpression');
        }
        $this->fields[] = $field;
        return $this;
    }
    public function removeField($name)
    {
        foreach ($this->fields as $key=>$field)
        {
            if ($field->identifier == $name)
            {
                unset($this->fields[$key]);
            }
        }
        return $this;
    }
    public function getFields()
    {
        return $this->fields;
    }
    public function setFields($fields)
    {
        $this->fields = array();
        if (is_array($fields))
        {
            foreach ($fields as $field)
            {
                $this->addField($field);
            }
        }
        else
        {
            $this->addField($fields);
        }
        return $this;
    }

    //</editor-fold>

    //<editor-fold defaultstate="collapsed" desc="GROUP BY clause">
    /**
     * Add GROUP BY expression.
     *
     * @param Element\iExpression $group
     * @return Query fluent interface
     */
    public function addGroup(Element\iExpression $group)
    {
        $this->group[] = $group;
        return $this;
    }

    /**
     * Get all GROUP BY expressions.
     *
     * @return Element\iExpression[]
     */
    public function getGroup()
    {
        return $this->group;
    }

    /**
     * add GROUP BY condition built from key==val associative array, like filter() for WHERE condition.
     *
     * @param array $condition
     * @return Select fluent interface
     */
    public function filterGroup($condition)
    {
        if (!is_array($condition)) $condition = func_get_args();
        foreach ($condition as $val)
        {
            $column = Element\ColumnIdentifier::create($val);
            $this->addGroup($column);
        }
        return $this;
    }

    /**
     * Set array of all GROUP BY expressions (previous expressions are rewritten).
     *
     * @param Element\iExpression[] array of expressions
     * @return Select fluent interface
     */
    public function setGroup(array $fields)
    {
        $this->group = array();
        if (is_array($fields))
        {
            foreach ($fields as $group)
            {
                $this->addGroup($group);
            }
        }
        else
        {
            $this->addGroup($fields);
        }
        return $this;
    }
    //</editor-fold>

    // <editor-fold defaultstate="collapsed" desc="HAVING clause">
    /**
     * Set HAVING condition (applied after GROUP BY and column aggregation functions)
     *
     * @param Element\iCondition|NULL having condition
     * @return Query fluent interface
     */
    public function setHaving(Element\iCondition $having = NULL)
    {
        $this->having = $having;
        return $this;
    }
    /**
     * Get current HAVING condition.
     *
     * @return Element\iCondition|NULL
     */
    public function getHaving()
    {
        return $this->having;
    }

    public function filterHaving($condition)
    {
        if (!is_array($condition)) $condition = func_get_args();
        foreach ($condition as $key=>$val)
        {
            if ($val instanceof WDB\Structure\FilterRule)
            {
                $column = Element\ColumnIdentifier::parse($val->column);
                $val = $val->value;
            }
            else
            {
                $column = Element\ColumnIdentifier::parse($key);
            }
            if (!$val instanceof Element\Datatype\iDatatype)
            {
                $val = Element\Datatype\AbstractType::createDatatype($val);
            }
            $this->addHavingCondition(Element\Compare::Equals($column, $val));
        }
        return $this;
    }

    public function notHaving(array $condition)
    {
        foreach ($condition as $key=>$val)
        {
            $this->addHavingCondition(Element\Compare::NEquals(Element\ColumnIdentifier::parse($key), WDB\Query\Element\Datatype\AbstractType::createDatatype($val)));
        }
        return $this;
    }

    public function addHavingCondition(Element\iCondition $condition)
    {
        if ($this->having === NULL)
        {
            $this->having = $condition;
        }
        else
        {
            $this->having = Element\LogicOperator::lAnd($this->having, $condition);
        }
        return $this;
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="UNION clause">
    /**
     * Appends an union after this query.
     *
     * Unions in WDB are built the way that you append one select query to another with this method.
     *
     * @param WDB\Query\Select select to append after this with union
     * @param bool If true, union returns all records returned in both selects (UNION ALL clause). If false,
     *  returns only distinct rows (UNION DISTINCT clause)
     */
    public function setUnion(WDB\Query\Select $unionTo, $all = TRUE)
    {
        $this->union = $unionTo;
        $this->unionAll = $all;
    }

    /**
     * Get current Select object bound to this one with union (or NULL if no select is bound)
     *
     * @return WDB\Query\Select|NULL
     */
    public function getUnion()
    {
        return $this->union;
    }

    /**
     * Returns true if current union is configured as UNION ALL (false means DISTINCT)
     *
     * @return bool
     */
    public function getUnionAll()
    {
        return $this->unionAll;
    }

    /**
     * Remove select appended to this one with union.
     */
    public function removeUnion()
    {
        $this->union = NULL;
    }

    //</editor-fold>

    // <editor-fold defaultstate="collapsed" desc="\Countable impl.">
    /**
     * Return count of rows returned when executing this query.
     *
     * @param WDB\Database database connection
     * @return int
     */
    public function count($database = NULL)
    {
        $c = clone $this;
        $c->setFields(new Element\SelectField(Element\DBFunction::countRows()));
        $c->limit = NULL;
        $c->offset = NULL;
        return $c->run($database)->singleValue();
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="\WDB\iDatasource impl.">

    /**
     * update configuration of limit and offset when page or pagesize changes
     */
    protected function updatePage()
    {
        $this->limit = $this->pageSize;
        $this->offset = $this->page * $this->pageSize;
    }

    public function page($number)
    {
        $this->page = $number;
        $this->updatePage();
        return $this;
    }

    public function getPage()
    {
        return $this->page;
    }

    public function select()
    {
        $fields = WDB\Utils\Arrays::linearize(func_get_args());
        foreach ($this->fields as $field)
        {
            if ($field->identifier === NULL) continue;
            if (!in_array($field->identifier, $fields))
            {
                $this->removeField($field->identifier);
            }
            else
            {
                unset($fields[array_search($field->identifier, $fields)]);
            }
        }
        if (count($fields) > 0)
        {
            throw new Exception\BadArgument('Column(s) '.implode(', ', $fields).' not found in query');
        }
        return $this;
    }

    public function getPageSize()
    {
        return $this->pageSize;
    }

    public function pageSize($number)
    {
        $this->pageSize = $number;
        $this->updatePage();
        return $this;
    }

    public function sort($key, $asc = true, $prepend = true)
    {
        $this->prependOrder(new Element\OrderRule(
                Element\ColumnIdentifier::parse($key), $asc ? Element\OrderRule::ASC : Element\OrderRule::DESC));
        return $this;
    }

    public function subset($offset, $count)
    {
        $this->limit = $count;
        $this->offset = $offset;
        return $this;
    }

    public function fetch()
    {
        return $this->run();
    }

    public function distinct($distinct = TRUE)
    {
        $this->setDistinct($distinct);
        return $this;
    }

    // </editor-fold>

    //<editor-fold defaultstate="collapsed" desc="DISTINCT flag">
    public function getDistinct()
    {
        return $this->distinct;
    }

    public function setDistinct($distinct)
    {
        $this->distinct = (bool)$distinct;
    }
    //</editor-fold>

    /**
     * Perform this select on a databse.
     *
     * @param WDB\Database|NULL
     * @return iSelectResult
     * @throws Exception\InvalidOperation
     */
    public function run(WDB\Database $database = NULL)
    {
        if (count($this->fields) == 0) throw new Exception\InvalidOperation
                ('cannot execute select query with no column');
        return parent::run($database);
    }

    // <editor-fold defaultstate="collapsed" desc="Element\iExpression impl.">
    public function toSQL(\WDB\SQLDriver\iSQLDriver $driver)
    {
        return $driver->tosql_subselect($this);
    }
    // </editor-fold>

    public function alias($alias)
    {
        return AliasedTableSource::create($this, $alias);
    }
}
